package com.abel.bigwater.wflow

import com.abel.bigmeter.service.DmaParam
import com.abel.bigwater.model.BwDma
import com.abel.bigwater.model.ticket.LossTicket
import com.abel.bigwater.model.ticket.TicketParam
import com.abel.bigwater.model.zone.MINI_DELTA
import com.abel.bigwater.wflow.file.PhotoBucket
import com.abel.bigwater.wflow.file.encodePath
import com.abel.bigwater.wflow.mapper.MeterMapper
import com.abel.bigwater.wflow.mapper.StatMapper
import com.abel.bigwater.wflow.mapper.TicketMapper
import org.activiti.api.process.model.ProcessInstance
import org.activiti.api.process.model.builders.*
import org.activiti.api.process.runtime.ProcessRuntime
import org.activiti.api.runtime.shared.query.Pageable
import org.activiti.api.task.model.Task
import org.activiti.api.task.model.builders.ClaimTaskPayloadBuilder
import org.activiti.api.task.model.builders.CompleteTaskPayloadBuilder
import org.activiti.api.task.model.builders.DeleteTaskPayloadBuilder
import org.activiti.api.task.model.builders.GetTasksPayloadBuilder
import org.activiti.api.task.runtime.TaskRuntime
import org.activiti.engine.ProcessEngine
import org.activiti.engine.RuntimeService
import org.activiti.engine.impl.identity.Authentication
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity
import org.joda.time.DateTime
import org.joda.time.Duration
import org.joda.time.format.ISODateTimeFormat
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryCollection
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.io.WKTReader
import org.locationtech.jts.io.WKTWriter
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.multipart.MultipartHttpServletRequest
import java.util.*
import java.util.stream.Collectors
import javax.servlet.http.HttpServletRequest
import kotlin.streams.toList

@Controller
@RequestMapping("lossMng")
class LossMngController {
    @Autowired
    var processRuntime: ProcessRuntime? = null

    @Autowired
    var taskRuntime: TaskRuntime? = null

    @Autowired
    var processEngine: ProcessEngine? = null

    @Autowired
    var runtimeService: RuntimeService? = null

/*
    @Autowired
    var securityUtil: SecurityUtil? = null
*/

    @Autowired
    var meterMapper: MeterMapper? = null

    @Autowired
    var statMapper: StatMapper? = null

    @Autowired
    var ticketMapper: TicketMapper? = null

    @Autowired
    var photoBucket: PhotoBucket? = null

    private fun initConfig() {
    }

    @RequestMapping("/procList")
    fun procList(m: Model): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val plist = processRuntime?.processDefinitions(Pageable.of(0, 20))
        m.set("pdList", plist?.content.orEmpty())
        val pd = plist?.content?.firstOrNull()

        return "proc-list"
    }

    @RequestMapping("/undeploy/{pdId}")
    fun undeployProcDef(m: Model, @PathVariable("pdId") pdId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val errors = arrayListOf<String>()
        m.set("errors", errors)

        val pd = processEngine?.repositoryService?.getProcessDefinition(pdId)
//        if (me.roles?.find { it.name == ROLE_ADMIN } == null || "1" != me.firmId) {
        if (me.id != LOGIN_ADMIN || pd == null) {
            errors.add("您无权卸载流程模板: ${pd?.name}")

            val plist = processRuntime?.processDefinitions(Pageable.of(0, 20))
            m.set("pdList", plist?.content.orEmpty())
//            val pd = plist?.content?.firstOrNull()

            return "proc-list"
        }

        // verify if the process has instances.
        val cnt = runtimeService?.createProcessInstanceQuery()
                ?.processDefinitionId(pd.id)?.processDefinitionVersion(pd.version)?.count()
        if ((cnt ?: 0) > 0) {
            val msg = "无法卸载模板 ${pd.name}-${pd.version}: 请完成或删除模板对应的流程实例: ${cnt}."
            lgr.warn(msg)
            errors.add(msg)

            return "/lossMng/procList"
        }

        // proceed to undeploy
        lgr.info("undeploying ${pd.javaClass} with ${pdId}")
        if (pd is ProcessDefinitionEntity) {
            try {
                processEngine?.repositoryService?.deleteDeployment(pd.deploymentId)

                errors.add("卸载流程模板: ${pdId} with ${pd.deploymentId}")
            } catch (ex: Exception) {
                errors.add("是否流程实例未结束？${ex.message}")
                lgr.error("卸载失败: ${ex.message}", ex)
            }
        } else {
            errors.add("流程模板无法卸载: ${pd} ~ ${pdId}")
        }

        return "/lossMng/procList"
    }

    /**
     * DMA列表
     */
    @RequestMapping("/dmaList")
    fun dmaList(m: Model): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val plist = processRuntime?.processDefinitions(Pageable.of(0, 20),
                GetProcessDefinitionsPayloadBuilder()
                        .withProcessDefinitionKey("procLossMng")
                        .build())

        // 取版本号最大
        val proc = plist?.content?.maxBy { it.version }

        val lossList = statMapper?.sumLossList(DmaParam(firmId = me.firmId).apply {
            statDate1 = DateTime.now().withTimeAtStartOfDay().minusDays(8).toDate()
            statDate2 = DateTime.now().withTimeAtStartOfDay().minusDays(1).toDate()
        })

        m.set("proc", proc!!)
        m.set("lossList", lossList.orEmpty().sortedByDescending { it.lossPerHour })

        return "dma-list"
    }

    /**
     * 实例清单
     */
    @RequestMapping("/pinstList")
    fun pinstList(m: Model): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val qry = if (me.firmId!!.startsWith("1"))
            runtimeService?.createProcessInstanceQuery()?.list().orEmpty()
        else
            runtimeService?.createProcessInstanceQuery()
                    ?.variableValueLike("firmId", me.firmId + "%")
                    ?.list().orEmpty()

        m.set("pinstList", qry)

        return "pinst-list"
    }

    /**
     * 单个实例的子任务
     */
    @RequestMapping("/subtaskList/{pinstId}")
    fun pinstTaskList(m: Model, @PathVariable("pinstId") pinstId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val pinst = processRuntime?.processInstance(pinstId)

        val tlist = taskRuntime?.tasks(Pageable.of(0, 20),
                GetTasksPayloadBuilder().withProcessInstanceId(pinstId).build())

        m.set("pinst", pinst!!)
        m.set("taskList", tlist?.content.orEmpty())

        val olist = taskRuntime?.tasks(Pageable.of(0, 20),
                GetTasksPayloadBuilder().withGroups(listOf("leader", "FirmLeader", "deptLeader")).build())

        m.set("otherList", olist?.content.orEmpty())

        lgr.info("sub list: ${tlist?.totalItems}, other list: ${olist?.totalItems}")

        return "pinst-task-list"
    }

    /**
     * 工单列表
     */
    @RequestMapping("/ticketList")
    fun ticketList(m: Model, @RequestParam("dmaId", required = false) _dmaId: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val tlist = ticketMapper?.listTicket(TicketParam().apply {
            firmId = me.firmId
            dmaId = if (_dmaId.isNullOrBlank()) null else _dmaId
        })

        // 编码URI
        tlist?.forEach { it.encodePath(photoBucket) }

        m.set("ticketList", tlist.orEmpty())

        // dma
        val dlist = meterMapper!!.selectDma(me.firmId, null, null)
        val dmaList = arrayListOf<BwDma>().apply { addAll(dlist) }
        dmaList.add(0, BwDma().apply {
            id = ""
            name = "全部"
        })
        m.set("dmaList", dmaList)

        return "ticket-list"
    }

    /**
     * 工单列表
     */
    @RequestMapping("/ticketStatByDma")
    fun ticketStatByDma(m: Model, @RequestParam("dmaId", required = false) _dmaId: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val tlist = ticketMapper?.statTicketByDma(TicketParam().apply {
            firmId = me.firmId
            dmaId = if (_dmaId.isNullOrBlank()) null else _dmaId
        })

        m.set("tsList", tlist.orEmpty())

        return "ticket-stat-by-dma"
    }

    /**
     * 工单列表
     */
    @RequestMapping("/ticketStatByStuff")
    fun ticketStatByStuff(m: Model, @RequestParam("dmaId", required = false) _dmaId: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val tlist = ticketMapper?.statTicketByStuff(TicketParam().apply {
            firmId = me.firmId
            dmaId = if (_dmaId.isNullOrBlank()) null else _dmaId
        })

        m.set("tsList", tlist.orEmpty())

        return "ticket-stat-by-stuff"
    }

    /**
     * 工单列表
     */
    @RequestMapping("/ticketStatByLossReason")
    fun ticketStatByLossReason(m: Model, @RequestParam("dmaId", required = false) _dmaId: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val tlist = ticketMapper?.statTicketByLossReason(TicketParam().apply {
            firmId = me.firmId
            dmaId = if (_dmaId.isNullOrBlank()) null else _dmaId
        })

        m.set("tsList", tlist.orEmpty())

        return "ticket-stat-by-loss-reason"
    }

    /**
     * 工单列表
     */
    @RequestMapping("/ticketStatByCancelReason")
    fun ticketStatByCancelReason(m: Model, @RequestParam("dmaId", required = false) _dmaId: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val tlist = ticketMapper?.statTicketByCancelReason(TicketParam().apply {
            firmId = me.firmId
            dmaId = if (_dmaId.isNullOrBlank()) null else _dmaId
        })

        m.set("tsList", tlist.orEmpty())

        return "ticket-stat-by-cancel-reason"
    }

    /**
     * 签收派单。
     */
    @RequestMapping("/loadDma/{pid}")
    fun loadDma(m: Model, @PathVariable("pid") pid: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val pd = processRuntime?.processDefinition(pid)

        m.set("pd", pd!!)

        val my = DemoApplicationConfiguration.myself()!!
        val lossList = statMapper?.sumLossList(DmaParam(firmId = my.firmId).apply {
            statDate1 = DateTime.now().withTimeAtStartOfDay().minusDays(8).toDate()
            statDate2 = DateTime.now().withTimeAtStartOfDay().minusDays(1).toDate()
        })
        m.set("lossList", lossList.orEmpty().sortedByDescending { it.lossPerHour })

        return "claim-start-load-dma"
    }

    /**
     * 完成启动流程
     */
    @RequestMapping("/startProc/{pid}")
    fun startProc(m: Model, @PathVariable("pid") pid: String, @RequestParam("dmaId") _dmaId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val user = SecurityContextHolder.getContext().authentication.principal as UserDetails

        val pd = processRuntime?.processDefinition(pid)
        lgr.info("${user.username} is trying to start process ${pd?.name} (${pd?.id})...")

        m.set("pd", pd!!)

        // 查询DMA
        val dma = meterMapper?.selectDmaById(_dmaId)

        // 查看有无工单
        val ticketList = ticketMapper?.listTicket(TicketParam().apply {
            dmaId = _dmaId
            rows = 1
        })
        var ticket = ticketList?.firstOrNull()
        val torder = "0${(ticket?.ticketId?.takeLast(2)?.toInt() ?: 0) + 1}".takeLast(2)

        var pinst: ProcessInstance? = null
        if (ticketList?.isNotEmpty() == true && ticket!!.curStep !in listOf(
                        ProcessInstance.ProcessInstanceStatus.DELETED.toString(),
                        ProcessInstance.ProcessInstanceStatus.CANCELLED.toString(),
                        ProcessInstance.ProcessInstanceStatus.COMPLETED.toString()
                )) {

            // query working process-instance.
            val ilist = processRuntime?.processInstances(Pageable.of(0, 10),
                    GetProcessInstancesPayloadBuilder().withBusinessKey(ticket.ticketId)
                            .withProcessDefinitionKey(pd.key).build())

            if (ilist?.totalItems == 0) {
                // 流程实例缺失，则结束工单并重启
                ticket.curStep = ProcessInstance.ProcessInstanceStatus.DELETED.toString()
                ticketMapper?.updateTicketByValue(ticket)

                ticket = null
            } else {
                for (p1 in ilist?.content.orEmpty()) {
                    if (p1.status in listOf(ProcessInstance.ProcessInstanceStatus.RUNNING,
                                    ProcessInstance.ProcessInstanceStatus.SUSPENDED)) {
                        // 找到工单
                        pinst = p1

                        // 如果挂起则恢复
                        if (pinst?.status == ProcessInstance.ProcessInstanceStatus.SUSPENDED)
                            processRuntime?.resume(ResumeProcessPayloadBuilder().withProcessInstance(pinst).build())

                        m.set("pinst", pinst)

                        m.set("ticket", ticket)

                        break
                    }
                }
            }
        }


        if (pinst == null) {
            // the value will be set to activiti::initiator, here is applyUserId.
            // then it will be used in the following tasks.
            Authentication.setAuthenticatedUserId(user.username)
            val finderIdList = DemoApplicationConfiguration.myColleges().orEmpty()
                    .stream().map { it.id }.collect(Collectors.toList())


            // 查询最近7日漏损
            val lossList = statMapper?.selectLoss(DmaParam(dmaId = _dmaId).apply {
                statDate1 = DateTime.now().withTimeAtStartOfDay().minusDays(8).toDate()
                statDate2 = DateTime.now().withTimeAtStartOfDay().minusDays(1).toDate()
            })

            val startLob = lossList?.firstOrNull()
            val maxLob = lossList?.maxBy { it.lossPerHour ?: 0.0 }

            // 创建工单
            ticket = LossTicket().apply {
                dmaId = _dmaId
                dmaName = dma?.name ?: ""
                firmId = dma?.firmId ?: ""

                curStep = ProcessInstance.ProcessInstanceStatus.CREATED.toString()

                startDate = startLob?.statDate ?: DateTime.now().withTimeAtStartOfDay().minusDays(1).toDate()
                startLoss = startLob?.lossPerHour
                maxLoss = maxLob?.lossPerHour

                startId = user.username
                startTime = DateTime.now().toDate()

                pdId = pd.id
                pdName = pd.name
                pdVersion = "${pd.version}"

                // geometry.
                if (dma?.dmaLoc?.isNotBlank() == true) {
                    try {
                        val geom = WKTReader().read(dma.dmaLoc)
                        if (geom != null) {
                            dmaLoc = WKTWriter().write(GeometryFactory().createGeometryCollection(arrayOf(geom)))
                        }
                    } catch (ex: Exception) {
                        lgr.error("bad geom-json: ${dma.dmaLoc}")
                    }
                }
            }

            ticket.ticketId = "${_dmaId}@${DateTime(ticket.startDate).toString(ISODateTimeFormat.basicDate())}@$torder"
            val msg = "${ticket.dmaName}(${me.firmName}):${user.username}于${DateTime.now().toString(ISODateTimeFormat.dateHourMinute())}发起DMA探漏"

            // 启动流程实例
            pinst = processRuntime?.start(StartProcessPayloadBuilder().withProcessDefinitionId(pid)
                    .withBusinessKey(ticket.ticketId)
                    .withVariable("firmId", me.firmId)
                    .withVariable("dmaId", ticket.dmaId)
                    .withVariable("dmaName", ticket.dmaName)
                    .withVariable("startLossDate", DateTime(ticket.startDate).toString(ISODateTimeFormat.basicDateTime()))
                    .withVariable("startLoss", ticket.startLoss)
                    .withVariable("maxLossDate", DateTime(ticket.startDate).toString(ISODateTimeFormat.basicDateTime()))
                    .withVariable("maxLoss", ticket.maxLoss)
                    .withVariable("deptLeaderIdList", finderIdList)
                    // .withProcessInstanceName(ticket.dmaName)
                    .withName(msg)
                    .withVariable("description", ticket.dmaName)
                    .build())

            // 设置描述
            pinst = processRuntime?.update(UpdateProcessPayloadBuilder()
                    .withProcessInstanceId(pinst!!.id)
                    .withDescription(msg)
                    .build())

            // 保存工单
            ticket.pinstId = pinst!!.id
            ticketMapper!!.createTicket(ticket)

            m.set("ticket", ticket)
            m.set("pinst", pinst!!)
        }

        return "complete-start-proc"
    }

    /**
     * 挂起流程实例
     */
    @RequestMapping("/suspendInst/{pinstId}")
    fun suspendInst(m: Model, @PathVariable("pinstId") pinstId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val pinst = processRuntime?.processInstance(pinstId)
        if (pinst != null && (pinst.status == ProcessInstance.ProcessInstanceStatus.CREATED
                        || pinst.status == ProcessInstance.ProcessInstanceStatus.RUNNING)) {
            processRuntime?.suspend(SuspendProcessPayloadBuilder().withProcessInstanceId(pinstId).build())
        }

        return "/lossMng/pinstList"
    }

    /**
     * 删除流程实例
     */
    @RequestMapping("/deleteInst/{pinstId}")
    fun deleteInst(m: Model, @PathVariable("pinstId") _pinstId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN
        if (me.roles?.find { it.name == ROLE_ADMIN } == null) {
            m.set("errors", listOf("您无权中断流程，请跟水司领导联系."))
            return "/lossMng/taskList"
        }

        val pinst = processRuntime?.processInstance(_pinstId)

        var ticket: LossTicket? = null
        if (true == pinst?.processDefinitionKey?.startsWith(PROC_LOSS_MNG)) {
            ticket = ticketMapper?.ticketById(TicketParam().apply { pinstId = pinst.id })
        }

        if (ticket != null) {
            m.set("tp", TaskProcInfo(null, pinst))
            return VIEW_CANCEL_LOSS
        }

        // 工单不存在，直接删除流程
        if (pinst != null && pinst.status != ProcessInstance.ProcessInstanceStatus.DELETED) {
            processRuntime?.delete(DeleteProcessPayloadBuilder().withProcessInstanceId(_pinstId).build())

            m.set("errors", arrayOf("删除正在进行的流程: ${pinst.name}"))
        }

        return "/lossMng/pinstList"
    }

    /**
     * 签收任务，根据任务名称定向到不同的视图
     */
    @RequestMapping("/cancelTask/{taskId}")
    fun cancelTask(m: Model, @PathVariable("taskId") taskId: String, req: HttpServletRequest): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN
        if (me.roles?.find { it.name == ROLE_ADMIN } == null) {
            m.set("errors", listOf("您无权中断任务，请跟水司领导联系."))
            return "/lossMng/taskList"
        }

        val task: Task?
        try {
            task = taskRuntime?.task(taskId)
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set("errors", listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }

        val pinst = processRuntime?.processInstance(task?.processInstanceId)

        var ticket: LossTicket? = null
        if (true == pinst?.processDefinitionKey?.startsWith(PROC_LOSS_MNG)) {
            ticket = ticketMapper?.ticketById(TicketParam().apply { pinstId = pinst.id })
        }

        if (ticket != null) {
            // 工单存在，则填写取消原因
            m.set("tp", TaskProcInfo(task, pinst))
            return VIEW_CANCEL_LOSS
        } else {
            // 工单不存在，直接取消
            processRuntime?.delete(DeleteProcessPayloadBuilder().withProcessInstance(pinst).build())
            lgr.info("cancelled task ${taskId} and its process instance ${pinst!!.id}: ${pinst.name}")
        }

        return RM_TASK_LIST
    }

    /**
     * 取消工单
     */
    @RequestMapping("cancelTicket/{pinstId}")
    fun cancelTicket(m: Model, @PathVariable("pinstId") _pinstId: String,
                     @RequestParam("cancelReason") _cancelReason: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN
        if (me.roles?.find { it.name == ROLE_ADMIN } == null) {
            m.set("errors", listOf("您无权中断流程，请跟水司领导联系."))
            return "/lossMng/taskList"
        }

        val pinst = processRuntime!!.processInstance(_pinstId)
        if (_cancelReason.isNullOrBlank()) {
            m.set(ERRORS_KEY, arrayOf("请填写终止原因"))

            m.set("tp", TaskProcInfo(null, pinst))
            return VIEW_CANCEL_LOSS
        }

        processRuntime!!.delete(DeleteProcessPayloadBuilder().withProcessInstanceId(_pinstId)
                .withReason(_cancelReason).build())

        var ticket: LossTicket? = null
        if (true == pinst?.processDefinitionKey?.startsWith(PROC_LOSS_MNG)) {
            val cnt = ticketMapper?.updateTicketByValue(LossTicket().apply {
                pinstId = _pinstId
                cancelReason = _cancelReason
                cancelTime = Date()
                canceller = me.id
            })

            lgr.info("cancel ticket ${_pinstId} count: ${cnt}")
        }

        return RM_TASK_LIST
    }

    /**
     * 任务清单。
     */
    @RequestMapping("taskList")
    fun taskList(m: Model): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val tlist = taskRuntime?.tasks(Pageable.of(0, 20),
                GetTasksPayloadBuilder().build())
        m.set("taskList", tlist?.content.orEmpty().stream().map { it ->
            try {
                val pinst = processRuntime?.processInstance(it.processInstanceId)
                TaskProcInfo(it, pinst)
            } catch (ex: Exception) {
                lgr.error("fail to load task ${it.id} with proc-instance: ${it.processInstanceId} caused by ${ex.message}")

                try {
                    taskRuntime?.delete(DeleteTaskPayloadBuilder()
                            .withTaskId(it.id)
                            .withReason("no process instance ${it.processInstanceId} related to this task: ${it.id}~${it.name}")
                            .build())
                } catch (ex: Exception) {
                    lgr.error("fail to delete task ${it.id}~${it.name} caused by ${ex.message}: ${it.assignee}")
                }

                null
            }
        }.toList().filter { it != null })
        lgr.info("task-list: ${tlist?.totalItems}")

        val t = tlist?.content?.firstOrNull()

        return "task-list"
    }

    /**
     * 签收任务，根据任务名称定向到不同的视图
     */
    @RequestMapping("/claimTask/{taskId}")
    fun claimTask(m: Model, @PathVariable("taskId") taskId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        var task: Task?
        try {
            task = taskRuntime?.task(taskId)
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set("errors", listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }

        val pinst = processRuntime?.processInstance(task?.processInstanceId)
        lgr.info("工单KEY: ${pinst?.businessKey}")

        if (task == null || pinst == null) {
            m.set("errors", listOf("流程或任务ID不存在：${taskId}"))

            return ERROR_LIST
        } else if (task.status != Task.TaskStatus.CREATED && task.status != Task.TaskStatus.ASSIGNED) {
            m.set("errors", listOf("任务无法再次分配"))

            return ERROR_LIST
        }

        m.set("task", task)

        lgr.info("proc-def key: ${pinst.processDefinitionKey}; task-name: ${task.name}")
        when (pinst.processDefinitionKey) {
            PROC_LOSS_MNG -> {
                val ticket = ticketMapper?.ticketById(TicketParam().apply { pinstId = pinst.id })?.apply {
                    encodePath(photoBucket)
                }
                if (ticket == null) {
                    m.set("errors", listOf("暂不支持的工单类型: ${task.name}, 工单号: ${pinst.businessKey}"))
                    return ERROR_LIST
                }

                when (task.name) {
                    TASK_ASSIGN_FINDER -> {
                        if (task.status != Task.TaskStatus.ASSIGNED) {
                            task = taskRuntime?.claim(ClaimTaskPayloadBuilder().withTaskId(taskId).build())
                        }

                        m.set("ticket", ticket)

                        m.set("finderList", DemoApplicationConfiguration.myColleges().orEmpty())
                        m.set("pinst", pinst)

                        return "claim-assign-finder"
                    }

                    TASK_FIND -> {
                        m.set("errors", listOf("探漏任务已分配给 ${task.assignee}."))
                        m.set("ticket", ticket)

                        return CLAIM_FIND
                    }

                    TASK_FIND_CHECK -> {
/*
                val tvs = taskRuntime?.variables(GetTaskVariablesPayloadBuilder()
                        .withTaskId(taskId)
                        .build())
                val findResult = tvs?.find { it.name == "findResult" }?.getValue() as LossFindFixResult
*/

                        m.set("ticket", ticket)

                        m.set("fixerList", DemoApplicationConfiguration.myColleges().orEmpty())

                        return CLAIM_FIND_CHECK
                    }

                    TASK_FIX -> {
                        m.set("ticket", ticket)

                        return CLAIM_FIX
                    }

                    TASK_FIX_CHECK -> {
                        m.set("ticket", ticket)

                        return CLAIM_FIX_CHECK
                    }
                }
            }
        }

        m.set("errors", listOf("暂不支持的任务类型: ${task.name}"))

        return ERROR_LIST
    }

    /**
     * 完成任务
     */
    @RequestMapping("/completeTask/{taskId}")
    fun completeTask(m: Model, @PathVariable("taskId") taskId: String,
                     @RequestParam("finderId", required = false) finderId: String?,
                     @RequestParam("findType", required = false) findType: String?,
                     @RequestParam("cancelReason", required = false) _cancelReason: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val task: Task?
        try {
            task = taskRuntime?.task(taskId)
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set("errors", listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }

        val pinst = processRuntime?.processInstance(task?.processInstanceId)
        if (task == null || pinst == null) {
            m.set("errors", listOf("流程或任务ID不存在：${taskId}"))

            return ERROR_LIST
        } else if (task.status != Task.TaskStatus.CREATED && task.status != Task.TaskStatus.ASSIGNED) {
            m.set("errors", listOf("任务无法再次分配"))

            return ERROR_LIST
        } else if (finderId.isNullOrBlank()) {
            m.set("errors", listOf("任务需要制定探漏队清单"))

            return ERROR_LIST
        }

        when (task.name) {
            TASK_ASSIGN_FINDER -> {
                return completeFindAssign(m, task, taskId, pinst, findType, finderId, _cancelReason)
            }

            else -> {
                m.set("errors", listOf("不支持的任务类型: ${task.name}"))

                return ERROR_LIST
            }
        }

    }

    /**
     * 完成：分配探漏队任务。
     */
    fun completeFindAssign(m: Model, task: Task, taskId: String, pinst: ProcessInstance,
                           findType: String?, _finderId: String?,
                           _cancelReason: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val type = if (findType.isNullOrBlank()) 2 else 1
        if (type == 2 && _cancelReason.isNullOrBlank()) {
            m.set(ERRORS_KEY, arrayOf("如果不需要探漏，请填写终止原因."))
            return ERROR_LIST
        }

        if (task.status == Task.TaskStatus.CREATED || task.status == Task.TaskStatus.ASSIGNED) {
            // 需要探漏
            taskRuntime?.complete(CompleteTaskPayloadBuilder().withTaskId(taskId)
                    .withVariable("dmaId", pinst.businessKey)
                    .withVariable("dmaName", pinst.businessKey)
                    .withVariable("findSummary", "findSummary-demo")
                    .withVariable("findLocation", "findLocation-demo")
                    .withVariable("estimateLoss", Random().nextDouble() * 100.0)
                    .withVariable("type", type)
                    .withVariable("finderId", _finderId)
                    .build())

            ticketMapper?.updateTicketByValue(LossTicket().apply {
                pinstId = pinst.id

                curStep = TASK_ASSIGN_FINDER
                senderId = DemoApplicationConfiguration.myself()?.id
                sendTime = DateTime.now().toDate()
                finderId = _finderId

                cancelReason = _cancelReason
                cancelTime = if (_cancelReason.isNullOrBlank()) null else Date()
            })
        }

        val errList = arrayListOf<String>()
        errList.add("探漏任务分配给 ${_finderId}: ${pinst.businessKey}.")

        m.set("errors", errList)
        return "/lossMng/taskList"
    }

    /**
     * 完成探漏
     * findLoc: can be point: POINT (112.1 35.6); or geomcollection:
     * GEOMETRYCOLLECTION (POINT (114 23), POINT (115 22), POINT (113 23), POINT (116.1 23.2))
     */
    @RequestMapping("/completeFind/{taskId}")
    fun completeFind(m: Model, @PathVariable("taskId") taskId: String,
                     @RequestParam("findLoss", required = false) _findLoss: String?,
                     @RequestParam("findSummary", required = false) _findSummary: String?,
                     @RequestParam("findLoc", required = false) _findLoc: String?,
                     @RequestParam("pipeSize", required = false) _pipeSize: String?,
                     @RequestParam("pipeStuff", required = false) _pipeStuff: String?,
                     @RequestParam(VAR_CHANGE_CONFIRM, required = false) changeConfirm: String?,
                     req: MultipartHttpServletRequest): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        lgr.info("探漏结果汇报: ${taskId}, ${_findSummary}")
        val errors = arrayListOf<String>()
        m.set("errors", errors)

        val task: Task?
        try {
            task = taskRuntime?.task(taskId)
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set("errors", listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }

        try {
            val pinst = processRuntime?.processInstance(task?.processInstanceId)
            if (pinst == null) {
                errors.add("未找到相应的流程实例: task.id=${taskId}")
                return ERROR_LIST
            }

            val ticket = ticketMapper?.ticketById(TicketParam().apply { pinstId = pinst.id })

            ticketMapper?.updateTicketByValue(LossTicket().apply {
                pinstId = pinst.id

                curStep = TASK_FIND

                // 首次设置时间
                claimFindTime = if (ticket?.claimFindTime == null) Date() else null

                // 确认设置时间
                findTime = if (changeConfirm.isNullOrBlank()) null else Date()

                try {
                    findLoss = _findLoss?.toDouble()
                } catch (ex: Exception) {
                    lgr.info("bad find-loss: ${_findLoss}")
                }
                findSummary = _findSummary
//                findLoc = _findLoc

                val photo1 = req.getFile("findPhoto1")
                val photo2 = req.getFile("findPhoto2")
                val photo3 = req.getFile("findPhoto3")
                findPhoto1 = photoBucket?.putPhoto(me.firmId, if (photo1?.isEmpty == true) null else photo1?.originalFilename, photo1?.inputStream)
                findPhoto2 = photoBucket?.putPhoto(me.firmId, if (photo2?.isEmpty == true) null else photo2?.originalFilename, photo2?.inputStream)
                findPhoto3 = photoBucket?.putPhoto(me.firmId, if (photo3?.isEmpty == true) null else photo3?.originalFilename, photo3?.inputStream)

                val photo1b = req.getFile("findPhoto1b")
                val photo2b = req.getFile("findPhoto2b")
                val photo3b = req.getFile("findPhoto3b")
                findPhoto1b = photoBucket?.putPhoto(me.firmId, if (photo1b?.isEmpty == true) null else photo1b?.originalFilename, photo1b?.inputStream)
                findPhoto2b = photoBucket?.putPhoto(me.firmId, if (photo2b?.isEmpty == true) null else photo2b?.originalFilename, photo2b?.inputStream)
                findPhoto3b = photoBucket?.putPhoto(me.firmId, if (photo3b?.isEmpty == true) null else photo3b?.originalFilename, photo3b?.inputStream)

                val plt1 = LocHelper.parseLoc(photo1?.inputStream, lgr) ?: LocHelper.parseLoc(photo1b?.inputStream, lgr)
                val plt2 = LocHelper.parseLoc(photo2?.inputStream, lgr) ?: LocHelper.parseLoc(photo2b?.inputStream, lgr)
                val plt3 = LocHelper.parseLoc(photo3?.inputStream, lgr) ?: LocHelper.parseLoc(photo3b?.inputStream, lgr)
                val pltList = arrayListOf<LocHelper.PhotoInfo>()
                if (plt1 != null) pltList.add(plt1)
                if (plt2 != null) pltList.add(plt2)
                if (plt3 != null) pltList.add(plt3)

                // union points if neccessary
                if (pltList.size > 0) {
                    // 根据照片信息设置位置及时间
                    findLoc = if (pltList.size == 1) WKTWriter().write(pltList.first().loc)
                    else WKTWriter().write(GeometryFactory().createGeometryCollection(pltList.map { it.loc }.toTypedArray()))
                    if (findTime != null) findTime = pltList.maxBy { it.time }!!.time
                } else if (!_findLoc.isNullOrBlank()) {
                    try {
                        val geom = WKTReader().read(_findLoc)

                        if (ticket?.findLoc.isNullOrBlank()) {
                            findLoc = _findLoc
                        } else {
                            // union the geom-collections.
                            val geomList = arrayListOf<Geometry>()
                            val gc1 = WKTReader().read(ticket!!.findLoc) as GeometryCollection
                            for (i in 1..gc1.numPoints) {
                                geomList.add(gc1.getGeometryN(i - 1))
                            }

                            // 替换上次最后一个坐标? 还是添加到最后一个坐标.
                            val ptLast = geomList.lastOrNull()
                            val pt = if (geom is GeometryCollection) geom.getGeometryN(geom.numPoints - 1) else geom
                            if (!pt.isEmpty) {
                                if (ptLast == null) {
                                    // 点集空
                                    geomList.add(pt)
                                } else if (ptLast.isEmpty) {
                                    // 最后一个点为空
                                    geomList[geomList.size - 1] = pt
                                } else {
                                    if (Math.abs(ptLast.coordinate.x - pt.coordinate.x) < MINI_DELTA
                                            && Math.abs(ptLast.coordinate.y - pt.coordinate.y) < MINI_DELTA) {
                                        // 非空且接近
                                        geomList[geomList.size - 1] = pt
                                    } else {
                                        // 非空不接近
                                        geomList.add(pt)
                                    }
                                }
                            }

                            // set union points to db.
                            findLoc = WKTWriter().write(GeometryFactory().createGeometryCollection(geomList.toTypedArray()))
                        }
                    } catch (ex: Exception) {
                        lgr.error("bad geom-json: $_findLoc")
                    }
                }

                pipeSize = _pipeSize
                pipeStuff = _pipeStuff
            })

            // 只提交照片
            if (changeConfirm.isNullOrBlank()) {
                return BURL_FIND + task!!.id
            }

            val t2 = taskRuntime?.complete(CompleteTaskPayloadBuilder().withTaskId(taskId)
                    .build())

            lgr.info("汇报结果: ${t2}, ${t2?.name} to ${t2?.assignee}")

            errors.add("探漏结果已汇报.")
        } catch (ex: Exception) {
            lgr.error("complete-find失败: ${ex.message}", ex)
            errors.add("汇报探漏成果失败: ${ex.message}")
        }

        return "/lossMng/taskList"
    }

    /**
     * 探漏审核完毕，将进入下一个任务.
     */
    @RequestMapping("/completeFindCheck/{taskId}")
    fun completeFindCheck(m: Model,
                          @PathVariable("taskId") taskId: String,
                          @RequestParam("needFix", required = false) needFix: String?,
                          @RequestParam("fixerId", required = false) _fixerId: String?,
                          @RequestParam("cancelReason") _cancelReason: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        lgr.info("探漏结果审核完毕：need-fix: ${needFix}, fixerId: ${_fixerId}")

        if (needFix.isNullOrBlank() && _cancelReason.isNullOrBlank()) {
            m.set(ERRORS_KEY, arrayOf("如果不需要维修，请填写终止原因."))
            return ERROR_LIST
        }

        val task: Task?
        try {
            task = taskRuntime?.task(taskId)
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set("errors", listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }

        val pinst = processRuntime?.processInstance(task?.processInstanceId)

        // 更新工单状态
        ticketMapper?.updateTicketByValue(LossTicket().apply {
            pinstId = pinst?.id
            fixerId = _fixerId
            curStep = TASK_FIND_CHECK

            cancelReason = if (needFix.isNullOrBlank()) _cancelReason else null
            cancelTime = if (needFix.isNullOrBlank()) Date() else null
        })

        val t2 = taskRuntime?.complete(CompleteTaskPayloadBuilder().withTaskId(taskId)
                .withVariable("fixerId", _fixerId)
                .withVariable("type", if (needFix?.isNotBlank() == true) 1 else 2)
//                .withVariable("findResult", findResult)
                .build())

        m.set("errors", listOf("探漏结果审核完毕: ${task?.id}"))

        return "/lossMng/taskList"
    }

    /**
     * 提交维修结果
     */
    @RequestMapping("/completeFix/{taskId}")
    fun completeFix(m: Model, @PathVariable("taskId") taskId: String,
                    @RequestParam("fixSummary") _fixSummary: String?,
                    @RequestParam("fixLoc", required = false) _fixLoc: String?,
                    @RequestParam("pipeSize", required = false) _pipeSize: String?,
                    @RequestParam("pipeStuff", required = false) _pipeStuff: String?,
                    @RequestParam("fixLoss") _fixLoss: String?,
                    @RequestParam("lossReason") _lossReason: String?,
                    @RequestParam(VAR_CHANGE_CONFIRM, required = false) changeConfirm: String?,
                    req: MultipartHttpServletRequest): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val task: Task?
        var pinst: ProcessInstance? = null
        try {
            task = taskRuntime?.task(taskId)
            pinst = processRuntime?.processInstance(task?.processInstanceId)
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set("errors", listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }
        val ticket = ticketMapper?.ticketById(TicketParam().apply { pinstId = pinst?.id })
        if (task == null || ticket == null) {
            return RM_TASK_LIST
        }
        m.set("ticket", ticket.apply {
            fixSummary = _fixSummary
            pipeSize = _pipeSize
            pipeStuff = _pipeStuff
            lossReason = _lossReason
            encodePath(photoBucket)
        })
        m.set("task", task)

        lgr.info("修漏结果汇报: ${taskId}, ${_fixSummary}")
        val errors = arrayListOf<String>()
        m.set(ERRORS_KEY, errors)

        if (!changeConfirm.isNullOrBlank()) {
            if (_pipeSize.isNullOrBlank()) {
                errors.add("请填写管径")
            }
            if (_pipeStuff.isNullOrBlank()) {
                errors.add("请填写管材")
            }
            if (_lossReason.isNullOrBlank()) {
                errors.add("请填写漏损原因")
            }
        }

        try {
            ticketMapper!!.updateTicketByValue(LossTicket().apply {
                pinstId = pinst!!.id

                curStep = TASK_FIX

                claimFixTime = if (ticket.claimFixTime == null) Date() else null
                fixTime = if (changeConfirm.isNullOrBlank()) null else Date()

                fixSummary = _fixSummary
                pipeSize = _pipeSize
                pipeStuff = _pipeStuff
                lossReason = _lossReason

                val photo1 = req.getFile("fixPhoto1")
                val photo2 = req.getFile("fixPhoto2")
                val photo3 = req.getFile("fixPhoto3")
                fixPhoto1 = photoBucket?.putPhoto(me.firmId, if (photo1?.isEmpty == true) null else photo1?.originalFilename, photo1?.inputStream)
                fixPhoto2 = photoBucket?.putPhoto(me.firmId, if (photo2?.isEmpty == true) null else photo2?.originalFilename, photo2?.inputStream)
                fixPhoto3 = photoBucket?.putPhoto(me.firmId, if (photo3?.isEmpty == true) null else photo3?.originalFilename, photo3?.inputStream)

                val photo1b = req.getFile("fixPhoto1b")
                val photo2b = req.getFile("fixPhoto2b")
                val photo3b = req.getFile("fixPhoto3b")
                fixPhoto1b = photoBucket?.putPhoto(me.firmId, if (photo1b?.isEmpty == true) null else photo1b?.originalFilename, photo1b?.inputStream)
                fixPhoto2b = photoBucket?.putPhoto(me.firmId, if (photo2b?.isEmpty == true) null else photo2b?.originalFilename, photo2b?.inputStream)
                fixPhoto3b = photoBucket?.putPhoto(me.firmId, if (photo3b?.isEmpty == true) null else photo3b?.originalFilename, photo3b?.inputStream)

                val photo1c = req.getFile("fixPhoto1c")
                val photo2c = req.getFile("fixPhoto2c")
                val photo3c = req.getFile("fixPhoto3c")
                fixPhoto1c = photoBucket?.putPhoto(me.firmId, if (photo1c?.isEmpty == true) null else photo1c?.originalFilename, photo1c?.inputStream)
                fixPhoto2c = photoBucket?.putPhoto(me.firmId, if (photo2c?.isEmpty == true) null else photo2c?.originalFilename, photo2c?.inputStream)
                fixPhoto3c = photoBucket?.putPhoto(me.firmId, if (photo3c?.isEmpty == true) null else photo3c?.originalFilename, photo3c?.inputStream)

                // photo location & timestamp
                val plt = LocHelper.parseLoc(photo1?.inputStream, lgr)
                        ?: LocHelper.parseLoc(photo1b?.inputStream, lgr)
                        ?: LocHelper.parseLoc(photo1c?.inputStream, lgr)
                val plt2 = LocHelper.parseLoc(photo2?.inputStream, lgr)
                        ?: LocHelper.parseLoc(photo2b?.inputStream, lgr)
                        ?: LocHelper.parseLoc(photo2c?.inputStream, lgr)
                val plt3 = LocHelper.parseLoc(photo3?.inputStream, lgr)
                        ?: LocHelper.parseLoc(photo3b?.inputStream, lgr)
                        ?: LocHelper.parseLoc(photo3c?.inputStream, lgr)

                // union the geom-collections.
                val pltList = arrayListOf<LocHelper.PhotoInfo>()
                if (plt != null) pltList.add(plt)
                if (plt2 != null) pltList.add(plt2)
                if (plt3 != null) pltList.add(plt3)

                // verify geom-json of fix-loc.
                if (pltList.size > 0) {
                    // 根据照片信息设置位置及时间
                    fixLoc = if (pltList.size == 1)WKTWriter().write(pltList.first().loc)
                        else WKTWriter().write(GeometryFactory().createGeometryCollection(pltList.map { it.loc }.toTypedArray()))
                    if (fixTime != null) fixTime = pltList.maxBy { it.time }!!.time
                } else if (!_fixLoc.isNullOrBlank()) {
                    try {
                        val geom = WKTReader().read(_fixLoc)

                        if (ticket?.fixLoc.isNullOrBlank()) {
                            fixLoc = _fixLoc
                        } else {
                            // union the geom-collections.
                            val geomList = arrayListOf<Geometry>()
                            val gc1 = WKTReader().read(ticket!!.fixLoc) as GeometryCollection
                            for (i in 1..gc1.numPoints) {
                                geomList.add(gc1.getGeometryN(i - 1))
                            }

                            // 替换上次最后一个坐标? 还是添加到最后一个坐标.
                            val ptLast = geomList.lastOrNull()
                            val pt = if (geom is GeometryCollection) geom.getGeometryN(geom.numPoints - 1) else geom
                            if (!pt.isEmpty) {
                                if (ptLast == null) {
                                    // 点集空
                                    geomList.add(pt)
                                } else if (ptLast.isEmpty) {
                                    // 最后一个点为空
                                    geomList[geomList.size - 1] = pt
                                } else {
                                    if (Math.abs(ptLast.coordinate.x - pt.coordinate.x) < MINI_DELTA
                                            && Math.abs(ptLast.coordinate.y - pt.coordinate.y) < MINI_DELTA) {
                                        // 非空且接近
                                        geomList[geomList.size - 1] = pt
                                    } else {
                                        // 非空不接近
                                        geomList.add(pt)
                                    }
                                }
                            }

                            // set union points to db.
                            fixLoc = WKTWriter().write(GeometryFactory().createGeometryCollection(geomList.toTypedArray()))
                        }
                    } catch (ex: Exception) {
                        lgr.error("bad geom-json: ${_fixLoc}")
                    }
                }

                try {
                    fixLoss = _fixLoss!!.toDouble()
                } catch (ex: Exception) {
                }
            })

            // 只提交照片
            val tk1 = ticketMapper!!.ticketById(TicketParam().apply { pinstId = pinst!!.id })?.apply { encodePath(photoBucket) }
            if (tk1 != null) {
                if (changeConfirm.isNullOrBlank() || (!changeConfirm.isNullOrBlank() && !errors.isEmpty())) {
                    m.set("ticket", tk1)
                    return CLAIM_FIX
                }
            }

            val t2 = taskRuntime?.complete(CompleteTaskPayloadBuilder().withTaskId(taskId)
                    .build())

            m.set("errors", listOf("维修 ${pinst?.businessKey} 结果: ${ticket?.fixSummary} 已提交."))
        } catch (ex: Exception) {
            lgr.error("complete-fix失败: ${ex.message}", ex)
            errors.add("汇报修漏成果失败: ${ex.message}")
        }

        return "/lossMng/taskList"
    }

    /**
     * 完成维修审核。
     */
    @RequestMapping("/completeFixCheck/{taskId}")
    fun completeFixCheck(m: Model, @PathVariable("taskId") taskId: String,
                         @RequestParam("fixerId") fixerId: String?,
                         @RequestParam("pipeSize", required = false) _pipeSize: String?,
                         @RequestParam("pipeStuff", required = false) _pipeStuff: String?,
                         @RequestParam("lossReason") _lossReason: String?,
                         @RequestParam("confirmFixLoss") _confirmFixLoss: String?): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val errors = arrayListOf<String>()
        m.set(ERRORS_KEY, errors)
        if (_pipeSize.isNullOrBlank()) {
            errors.add("请填写管径")
        }
        if (_pipeStuff.isNullOrBlank()) {
            errors.add("请填写管材")
        }
        if (_lossReason.isNullOrBlank()) {
            errors.add("请填写漏损原因")
        }
        try {
            _confirmFixLoss!!.toDouble()
        } catch (ex: Exception) {
            errors.add("确认维修的漏失量不正确: ${_confirmFixLoss}")
        }

        val task: Task?
        val pinst: ProcessInstance?
        val ticket: LossTicket?
        try {
            task = taskRuntime?.task(taskId)
            pinst = processRuntime?.processInstance(task?.processInstanceId)
            ticket = ticketMapper?.ticketById(TicketParam().apply { pinstId = pinst!!.id })
        } catch (ex: Exception) {
            lgr.error("fail to fetch task ${taskId} caused by ${ex.message}")
            m.set(ERRORS_KEY, listOf("任务已完成(或不存在): ${taskId}"))
            return taskList(m)
        }

        lgr.info("维修结果 ${_confirmFixLoss} 已被 ${Authentication.getAuthenticatedUserId()} 确认.")

        if (errors.isNotEmpty() && ticket != null) {
            m.set("task", task!!)
            m.set("ticket", ticket.apply { encodePath(photoBucket) })
            return CLAIM_FIX_CHECK
        }

        // no error, or ticket absent.
        ticketMapper!!.updateTicketByValue(LossTicket().apply {
            pinstId = pinst?.id

            curStep = ProcessInstance.ProcessInstanceStatus.COMPLETED.toString()

            auditorId = DemoApplicationConfiguration.myself()?.id
            auditTime = DateTime.now().toDate()

            pipeSize = _pipeSize
            pipeStuff = _pipeStuff
            lossReason = _lossReason
            confirmFixLoss = _confirmFixLoss!!.toDouble()

            findDays = Duration(DateTime(sendTime), DateTime(findTime)).standardSeconds / 86400.0
            fixDays = Duration(DateTime(findTime), DateTime(fixTime)).standardSeconds / 86400.0
            val totalDays = Duration(DateTime(sendTime), DateTime(fixTime)).standardSeconds / 86400.0

            computeKpi(totalDays, confirmFixLoss ?: 0.0)
        })

        val t2 = taskRuntime?.complete(CompleteTaskPayloadBuilder().withTaskId(taskId)
                .build())

        m.set("errors", listOf("维修 ${pinst?.businessKey} 结果审核完毕: ${_confirmFixLoss} m³/d."))

        return "/lossMng/taskList"
    }

    /**
     * 显示进度图
     */
    @RequestMapping("/currentDiagram/{pinstId}")
    fun currentDiagram(m: Model, @PathVariable("pinstId") pinstId: String): String {
        val me = DemoApplicationConfiguration.myself()
        me ?: return URL_LOGIN

        val pd = processRuntime?.processInstance(pinstId)
        val aids = processRuntime?.processInstanceMeta(pinstId)?.activeActivitiesIds
        lgr.info("active activity id list: ${aids?.joinToString()}")

        val bmodel = processEngine?.repositoryService?.getBpmnModel(pd?.processDefinitionId)

        val giList = bmodel?.locationMap?.filterKeys { gk ->
            aids?.contains(gk) == true
        }?.values
        m.set("giList", giList!!)

        return "current-diagram"
    }

    companion object {
        private val lgr = LoggerFactory.getLogger(LossMngController::class.java)

        const val PROC_LOSS_MNG = "procLossMng"
        const val CLAIM_FIND = "claim-find"
        const val CLAIM_FIX = "claim-fix"
        const val CLAIM_FIND_CHECK = "claim-find-check"
        const val CLAIM_FIX_CHECK = "claim-fix-check"

        const val RM_TASK_LIST = "/lossMng/taskList"
        const val VIEW_CANCEL_LOSS = "complete-termination-proc"

        const val BURL_FIND = "/lossMng/claimTask/"
        const val BURL_FIX = "/lossMng/claimTask/"

        const val TASK_ASSIGN_FINDER = "任务分配"
        const val TASK_FIND = "探漏"
        const val TASK_FIND_CHECK = "探漏审核"
        const val TASK_FIX = "维修"
        const val TASK_FIX_CHECK = "维修审核"

        const val ERRORS_KEY = "errors"
        const val ERROR_LIST = "error-list"

        const val VAR_FIRM_ID = "firmId"

        /**
         * 确认完成探漏或维修.
         */
        const val VAR_CHANGE_CONFIRM = "changeConfirm"

        /**
         * 工单有效期
         */
        const val DAYS_EXPIRE: Int = 30

        const val URL_LOGIN = "redirect:/login"

        const val ROLE_ADMIN = "ADMINISTRATOR"

        const val LOGIN_ADMIN = "admin"
    }
}